<?php
//yubing@baixing.com

class Searcher {
  const READ_TIMEOUT = 1;
  const WRITE_TIMEOUT = 10;

  /*
   * Facet Types
   * mostly use as: Searcher::facet('user', 'city', new TrueQuery());
   * other usage: Searcher::facet('user', 'credit', new Query('city', '上海'), ['type' => 'histogram', 'interval' => 100]);
   * other usage: Searcher::facet('user', 'credit', new Query('city', '上海'),
   *                              ['type' => 'range', 'ranges' => [['to' => 100], ['from' => 100, 'to' => 500]]]);
   * return: Array of facet
   */
  private static $facetTypes = [
    'terms' => [
      'params' => ['size' => false],
      'response' => ['key' => 'terms', 'term' => 'term', 'count' => 'count']],
    'range' => [
      'params' => ['ranges' => true, 'size' => false],
      'response' => ['key' => 'ranges']],
    'histogram' => [//gap value facet, set interval as gap
      'params' => ['interval' => true, 'size' => false],
      'response' => ['key' => 'entries', 'term' => 'key', 'count' => 'count']],
    'date_histogram' => [//date gap value facet, set interval as day gap
      'params' => ['interval' => true, 'size' => false],
      'response' => ['key' => 'entries', 'term' => 'key', 'count' => 'count']],
  ];

  private static function buildQuery($type, $query) {
    return (new EsQueryBuilder())->query($query);
  }

  /**
   * @param $type
   * @param $class
   * @param $query
   * @param array $options
   * @return SearchResult
   */
  public static function query($type, $class, $query, $options = []) {

    /*
     * Default Values:
     * $esQuery 		: TrueQuery
     * */
    $esQuery = self::buildQuery($type, $query);
    $esFilter = isset($options['filter'])? self::buildQuery($type, $options['filter']) : null;
    $params = array(
      'from'	=>	0,
      'size'	=>	10,
      'sort'	=>	[],
    );
    $params['_source'] = false;
    $params['query'] = $esQuery;
    $params = array_merge($params, $options);
    $params['size'] = min(max($params['size'], 1), 1000);
    $params['from'] = min(max($params['from'], 0), 10000);
    if(!is_null($esFilter)){
      $params['filter'] = $esFilter;
    }

    //ElasticSearch only allow lowercase string for sort
    array_walk($params['sort'], function (&$v) {$v = strtolower($v);});

    $uri = self::locate($type) . "_search/";

    $startTime = microtime();
    $response = self::request($uri, $params);
    $costTime = dt($startTime, microtime());
    if ($costTime > 0.1 && !IS_CLI) {
      Log::set($class, intval($costTime * 1000) . 'ms : ' . json_encode($params), 'slowsearch');
    }
    InstantCounter::sum('esqtime', intval($costTime * 1000));

    return new ElasticSearchResult($response, $class);
  }

  public static function facet($type, $facet_field, $query, $options = []) {
    $esQuery = self::buildQuery($type, $query);
    $esFilter = isset($options['filter'])? self::buildQuery($type, $options['filter']) : null;
    $params = ['query' => $esQuery, 'size' => 0];
    if(!is_null($esFilter)){
      $params['filter'] = $esFilter;
    }

    $facetType = isset($options['type']) && isset(self::$facetTypes[$options['type']]) ? $options['type'] : 'terms';
    $allowParams = self::$facetTypes[$facetType]['params'];

    $params['facets']['facet'][$facetType] = ['field' => $facet_field];
    foreach ($allowParams as $key => $required) {
      if (isset($options[$key])){
        $params['facets']['facet'][$facetType][$key] = $options[$key];
      } elseif ($required) {
        throw new Exception("Need option:'{$key}' to get a {$facetType} facet");
      }
    }

    $uri = self::locate($type) . "_search/";
    $response = self::request($uri, $params, 10);
    $responseConf = self::$facetTypes[$facetType]['response'];
    $result = [];
    if ( $response
      && isset($response->facets->facet->$responseConf['key'])
      && is_array($response->facets->facet->$responseConf['key'])
    ) {
      foreach ($response->facets->facet->$responseConf['key'] as $item) {
        if (!isset($responseConf['term'])) {
          $result[] = $item;
        } else {
          $result[$item->$responseConf['term']] = $item->$responseConf['count'];
        }
      }
    }
    return $result;
  }

  public static function index($type, $doc = []) {
    if(!$doc) return false;	//过滤空数组，有些数据无需build或者数据有问题的，支持在NodeDoc::build()的时候返回空数组

    $startTime = microtime();
    $response = self::request(self::locate($type) . $doc['id'], $doc, self::WRITE_TIMEOUT);
    $costTime = dt($startTime, microtime());
    if ($costTime > 5) {
      Log::set($type, intval($costTime * 1000) . 'ms : ' . $doc['id'], 'slowindex');
    }
    InstantCounter::sum('esitime', intval($costTime * 1000));

    return $response;
  }

  public static function delete($type, $id) {
    if(!is_scalar($id) || strlen($id) == 0) return false; //id为空的delete会删掉整个Index

    if (($type == 'media' && strlen($id) != 10 && substr($id, 0, 2) != 'm_') ||
      ($type == 'user' && substr($id, 0, 2) != 'u_') ||
      ($type == 'position' && substr($id, 0, 2) != 'p_')
    ) {
      Log::set($type, $id, 'SearchDeleteERR');
      return true;
    }

    $res = self::request(self::locate($type) . strval($id), [], self::WRITE_TIMEOUT, 'delete');
    return $res;
  }

  public static function drop($type) {
    return self::request(self::locate($type), [], self::WRITE_TIMEOUT, 'delete');
  }

  private static function locate($type){
    if(!$type) throw new Exception("!!Error!! type not given!");
    return "matcha/{$type}/";
  }

  private static function request($uri, $params = [], $timeout = self::READ_TIMEOUT, $method = 'post') {
    $uri .= (strpos(rtrim($uri, '?&'), '?') === false) ? '?' : '&';

    $type = explode('/', $uri)[1];
    try {
      $host = Config::get("env.searcher.{$type}");
    } catch (Exception $e) {
      $host = Config::get("env.searcher.default");
    }

    $url = $host . $uri . 'timeout=' . $timeout . 's';
    try {
      $data = json_encode($params, JSON_UNESCAPED_UNICODE);
    } catch (Exception $e) {
      Email::send('hacker@mudlab9.com', 'invalid UTF8 in searcher', print_r($params, true));
      return false;
    }

    $body = EsHttpClient::$method($url, $data, $timeout);
    return json_decode($body);
  }
}

class EsHttpClient extends HttpClient{
  /**
   * Get the return of $url through HTTP DELETE Request
   * @param $url
   * @param int $timeout 单位是秒，1秒以内的用小数表示
   * @return bool|mixed
   */
  public static function delete($url, $timeout = 1) {
    $opts = [
      CURLOPT_HTTPGET => true,
      CURLOPT_CUSTOMREQUEST => "DELETE",
    ];
    return self::request($url, $opts, $timeout);
  }
}
